<?php  if ( ! defined('BASEPATH')) exit('禁止直接访问脚本目录！');
/**
 * CodeIgniter
 *
 * 这是一个PHP开源程序开发框架，要求PHP 5.1.6+
 *
 * @package		CodeIgniter
 * @author		ExpressionEngine Dev Team
 * @copyright	Copyright (c) 2008 - 2011, EllisLab, Inc.
 * @license		http://codeigniter.com/user_guide/license.html
 * @link		http://codeigniter.com
 * @since		Version 1.0
 * @filesource
 */

// ------------------------------------------------------------------------

/**
 * 安全类
 *
 * @package		CodeIgniter
 * @subpackage	Libraries
 * @category	Security
 * @author		ExpressionEngine Dev Team
 * @link		http://codeigniter.com/user_guide/libraries/security.html
 */
class CI_Security {

	/**
	 * 用随机哈希值来保护URL
	 *
	 * @var string
	 * @access protected
	 */
	protected $_xss_hash			= '';
	/**
	 * 用随机哈希值来保护--防御跨站请求伪造的cookie
	 *
	 * @var string
	 * @access protected
	 */
	protected $_csrf_hash			= '';
	/**
	 * 防御跨站请求伪造的cookie的过期时间，默认为7200秒
	 *
	 * @var int
	 * @access protected
	 */
	protected $_csrf_expire			= 7200;
	/**
	 * 防御跨站请求伪造的cookie的令牌名称
	 *
	 * @var string
	 * @access protected
	 */
	protected $_csrf_token_name		= 'ci_csrf_token';
	/**
	 * Cookie name for Cross Site Request Forgery Protection Cookie
	 * 防御跨站请求伪造的cookie的cookie名称
	 *
	 * @var string
	 * @access protected
	 */
	protected $_csrf_cookie_name	= 'ci_csrf_token';
	/**
	 * 决不允许的字符串列表
	 *
	 * @var array
	 * @access protected
	 */
	protected $_never_allowed_str = array(
					'document.cookie'	=> '[removed]',
					'document.write'	=> '[removed]',
					'.parentNode'		=> '[removed]',
					'.innerHTML'		=> '[removed]',
					'window.location'	=> '[removed]',
					'-moz-binding'		=> '[removed]',
					'<!--'				=> '&lt;!--',
					'-->'				=> '--&gt;',
					'<![CDATA['			=> '&lt;![CDATA[',
					'<comment>'			=> '&lt;comment&gt;'
	);

	/* 决不允许，正则表达式替换 */
	/**
	 * 决不允许的正则表达式替换列表
	 *
	 * @var array
	 * @access protected
	 */
	protected $_never_allowed_regex = array(
					"javascript\s*:"			=> '[removed]',
					"expression\s*(\(|&\#40;)"	=> '[removed]', // CSS 和 IE
					"vbscript\s*:"				=> '[removed]', // IE
					"Redirect\s+302"			=> '[removed]'
	);

	/**
	 * 构造函数
	 */
	public function __construct()
	{
		// CSRF(跨站请求伪造) 配置
		foreach(array('csrf_expire', 'csrf_token_name', 'csrf_cookie_name') as $key)
		{
			if (FALSE !== ($val = config_item($key)))
			{
				$this->{'_'.$key} = $val;
			}
		}

		// 附加应用程序特定的cookie前缀
		if (config_item('cookie_prefix'))
		{
			$this->_csrf_cookie_name = config_item('cookie_prefix').$this->_csrf_cookie_name;
		}

		// 设置 CSRF 哈希值
		$this->_csrf_set_hash();

		log_message('debug', "安全类初始化完成");
	}

	// --------------------------------------------------------------------

	/**
	 * 验证防御跨站请求伪造的cookie
	 *
	 * @return	object
	 */
	public function csrf_verify()
	{
		// 如果没有接到POST数据，那么我们设置CSRF cookie
		if (count($_POST) == 0)
		{
			return $this->csrf_set_cookie();
		}

		// 检测 POST 和 COOKIE 数组中是否都有口令卡
		if ( ! isset($_POST[$this->_csrf_token_name]) OR
			 ! isset($_COOKIE[$this->_csrf_cookie_name]))
		{
			$this->csrf_show_error();
		}

		// 检测口令卡是否匹配
		if ($_POST[$this->_csrf_token_name] != $_COOKIE[$this->_csrf_cookie_name])
		{
			$this->csrf_show_error();
		}

		// 为了不污染 POST 数组，我们在检测完成之后会对口令卡进行删除
		unset($_POST[$this->_csrf_token_name]);

		// 完成验证应及时销毁和更新
		unset($_COOKIE[$this->_csrf_cookie_name]);
		$this->_csrf_set_hash();
		$this->csrf_set_cookie();

		log_message('debug', "完成 CSRF 口令验证");

		return $this;
	}

	// --------------------------------------------------------------------

	/**
	 * 设置防御跨站请求伪造的cookie
	 *
	 * @return	object
	 */
	public function csrf_set_cookie()
	{
		$expire = time() + $this->_csrf_expire;
		$secure_cookie = (config_item('cookie_secure') === TRUE) ? 1 : 0;

		if ($secure_cookie)
		{
			$req = isset($_SERVER['HTTPS']) ? $_SERVER['HTTPS'] : FALSE;

			if ( ! $req OR $req == 'off')
			{
				return FALSE;
			}
		}

		setcookie($this->_csrf_cookie_name, $this->_csrf_hash, $expire, config_item('cookie_path'), config_item('cookie_domain'), $secure_cookie);

		log_message('debug', "完成 CRSF cookie 的设置");

		return $this;
	}

	// --------------------------------------------------------------------

	/**
	 * 显示 CSRF 错误
	 *
	 * @return	void
	 */
	public function csrf_show_error()
	{
		show_error('非法请求，已被禁止');
	}

	// --------------------------------------------------------------------

	/**
	 * 获取 CSRF 哈希值
	 *
	 * @return 	string 	self::_csrf_hash
	 */
	public function get_csrf_hash()
	{
		return $this->_csrf_hash;
	}

	// --------------------------------------------------------------------

	/**
	 * 获取 CSRF 令牌名称
	 *
	 * @return 	string 	self::csrf_token_name
	 */
	public function get_csrf_token_name()
	{
		return $this->_csrf_token_name;
	}

	// --------------------------------------------------------------------

	/**
	 * 过滤跨站脚本攻击(XSS)
	 *
	 * 通过过滤数据以防止跨站脚本攻击。该函数做了大量的比较彻底的工作，以图防止
	 * 那些甚至是最不起眼的XSS尝试。
	 *
	 * 注意：该函数应当被用来对提交上来的数据进行过滤，而不是用来对于平时运行数
	 * 据进行处理。
	 *
	 * 该函数的一部分是基于这里的思想来开发的：
	 * http://channel.bitflux.ch/wiki/XSS_Prevention
	 *
	 * 为了开发这个脚本，我参考了从其他众多项目搜集到的漏洞名单：
	 * http://ha.ckers.org/xss.html
	 *
	 * @param	mixed	字符串或者数组
	 * @param 	bool
	 * @return	string
	 */
	public function xss_clean($str, $is_image = FALSE)
	{
		/*
		 * 判断是否为数组
		 *
		 */
		if (is_array($str))
		{
			while (list($key) = each($str))
			{
				$str[$key] = $this->xss_clean($str[$key]);
			}

			return $str;
		}

		/*
		 * 删除不可见字符
		 */
		$str = remove_invisible_characters($str);

		// 验证URL中的实体部分
		$str = $this->_validate_entities($str);

		/*
		 * URL 解码
		 *
		 * 预防万一有这样的东西被提交:
		 *
		 * <a href="http://%77%77%77%2E%67%6F%6F%67%6C%65%2E%63%6F%6D">Google</a>
		 *
		 * 注意：采用 rawurldecode() 才不会导致加号被删除
		 *
		 */
		$str = rawurldecode($str);

		/*
		 * 将实体字符转换为 ASCII
		 *
		 * 这会使得我们的测试更加可靠。我们只会对标签内的实体部分进行转换，
		 * 因为这些才是导致安全问题的关键所在。
		 *
		 */

		$str = preg_replace_callback("/[a-z]+=([\'\"]).*?\\1/si", array($this, '_convert_attribute'), $str);

		$str = preg_replace_callback("/<\w+.*?(?=>|<|$)/si", array($this, '_decode_entity'), $str);

		/*
		 * 再次删除不可见的字符
		 */
		$str = remove_invisible_characters($str);

		/*
		 * 将所有的 Tab(水平制表符) 转换为空格
		 *
		 * 这可以避免出现这样的字符串：ja	vascript
		 * 注意：字符间的 space(空格)我们稍后才进行处理
		 * 注意：preg_replace 在处理大数据块的时候是极慢的，所以我们使用 str_replace。
		 * 
		 */

		if (strpos($str, "\t") !== FALSE)
		{
			$str = str_replace("\t", ' ', $str);
		}

		/*
		 * 捕获转换后的字符串为稍后的比较做准备
		 */
		$converted_string = $str;

		// 删除不被允许的字符串
		$str = $this->_do_never_allowed($str);

		/*
		 * 确保 PHP 标签安全
		 *
		 * 注意：XML 标签也会在不经意在被替换：
		 *
		 * <?xml
		 *
		 * 但它似乎并不构成问题。
		 */
		if ($is_image === TRUE)
		{
			// 由于大量的图片标签中通常都是使用的 PHP 短标签(编程习惯)，所以
			// 我们只对图片中的 PHP 长的开始标签进行处理，而不管短标签
			$str = preg_replace('/<\?(php)/i', "&lt;?\\1", $str);
		}
		else
		{
			$str = str_replace(array('<?', '?'.'>'),  array('&lt;?', '?&gt;'), $str);
		}

		/*
		 * 对被拆分开来的单词进行压缩处理
		 *
		 * 这可以对这样的单词起到纠正的作用：j a v a s c r i p t
		 * 处理之后会变成：javascript
		 * 
		 */
		$words = array(
				'javascript', 'expression', 'vbscript', 'script',
				'applet', 'alert', 'document', 'write', 'cookie', 'window'
			);

		foreach ($words as $word)
		{
			$temp = '';

			for ($i = 0, $wordlen = strlen($word); $i < $wordlen; $i++)
			{
				$temp .= substr($word, $i, 1)."\s*";
			}

			// 我们只想在随后出现的是一个非单词字符的时候这样子做
			// 这样才不会将像 "dealer to" 变成 "dealerto"
			$str = preg_replace_callback('#('.substr($temp, 0, -3).')(\W)#is', array($this, '_compact_exploded_words'), $str);
		}

		/*
		 * 删除存在于链接或者img标签中的不被允许的javascript
		 * 我们做了一些比较发现 stripos 函数慢得像坨屎一样，特别是当要匹配的部分存在于
		 * 被匹配的字符串中时尤其如此，所以我们使用了 preg_match()
		 */
		do
		{
			$original = $str;

			if (preg_match("/<a/i", $str))
			{
				$str = preg_replace_callback("#<a\s+([^>]*?)(>|$)#si", array($this, '_js_link_removal'), $str);
			}

			if (preg_match("/<img/i", $str))
			{
				$str = preg_replace_callback("#<img\s+([^>]*?)(\s?/?>|$)#si", array($this, '_js_img_removal'), $str);
			}

			if (preg_match("/script/i", $str) OR preg_match("/xss/i", $str))
			{
				$str = preg_replace("#<(/*)(script|xss)(.*?)\>#si", '[removed]', $str);
			}
		}
		while($original != $str);

		unset($original);

		// 删除那些邪恶的属性，例如 style，onclick 和 xmlns
		$str = $this->_remove_evil_attributes($str, $is_image);

		/*
		 * 过滤 HTML 标签
		 *
		 * 如果标签中含有下面列表中的任何词汇，该标签都会被转换掉
		 *
		 * So this: <blink>
		 * Becomes: &lt;blink&gt;
		 */
		$naughty = 'alert|applet|audio|basefont|base|behavior|bgsound|blink|body|embed|expression|form|frameset|frame|head|html|ilayer|iframe|input|isindex|layer|link|meta|object|plaintext|style|script|textarea|title|video|xml|xss';
		$str = preg_replace_callback('#<(/*\s*)('.$naughty.')([^><]*)([><]*)#is', array($this, '_sanitize_naughty_html'), $str);

		/*
		 * 过滤脚本元素
		 *
		 * 同上面类似。这里会寻找出不被允许的 PHP 和 javascript 命令，并将圆括号用
		 * 其他代替，是该命令不可执行。
		 *
		 * 例如:	eval('some code')
		 * 变成:		eval&#40;'some code'&#41;
		 */
		$str = preg_replace('#(alert|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)\((.*?)\)#si', "\\1\\2&#40;\\3&#41;", $str);


		// 最后的过滤
		// 这里增加一道额外的检测，以外漏网之鱼的出现
		$str = $this->_do_never_allowed($str);

		/*
		 * 图像以一种特殊的方式来处理
		 * 通常我们希望知道经过一系列的过滤我们到底发现了不安全因素没有呢。如果没有，
		 * 我们返回 TRUE，表示图像过滤完成。
		 * 我们通过将过滤前后的字符串进行比较就可以得出是否发现了 XSS，因为一旦携带了
		 * XSS 的字符串在过滤前后肯定是不一致的。
		 */

		if ($is_image === TRUE)
		{
			return ($str == $converted_string) ? TRUE: FALSE;
		}

		log_message('debug', "完成 XSS 过滤");
		return $str;
	}

	// --------------------------------------------------------------------

	/**
	 * 用随机哈希值保护 URL
	 *
	 * @return	string
	 */
	public function xss_hash()
	{
		if ($this->_xss_hash == '')
		{
			mt_srand();
			$this->_xss_hash = md5(time() + mt_rand(0, 1999999999));
		}

		return $this->_xss_hash;
	}

	// --------------------------------------------------------------------

	/**
	 * 对HTML实体进行解码
	 *
	 * 该函数用来代替 html_entity_decode()
	 *
	 * 我们之所以不仅仅使用 html_entity_decode()，是因为它自身的原因。
	 * 在技术上来说漏掉位于实体末尾的分号是不正确的做法，因为这样子处理的话，大多
	 * 数的浏览器仍然能够正确的解析和还原实体。而 html_entity_decode() 转换后的
	 * 实体仍然带有分号，所以我们用自己的方式来处理。
	 *
	 * @param	string
	 * @param	string
	 * @return	string
	 */
	public function entity_decode($str, $charset='UTF-8')
	{
		if (stristr($str, '&') === FALSE)
		{
			return $str;
		}

		$str = html_entity_decode($str, ENT_COMPAT, $charset);
		$str = preg_replace('~&#x(0*[0-9a-f]{2,5})~ei', 'chr(hexdec("\\1"))', $str);
		return preg_replace('~&#([0-9]{2,4})~e', 'chr(\\1)', $str);
	}

	// --------------------------------------------------------------------

	/**
	 * 文件名的安全
	 *
	 * @param	string
	 * @param 	bool
	 * @return	string
	 */
	public function sanitize_filename($str, $relative_path = FALSE)
	{
		$bad = array(
						"../",
						"<!--",
						"-->",
						"<",
						">",
						"'",
						'"',
						'&',
						'$',
						'#',
						'{',
						'}',
						'[',
						']',
						'=',
						';',
						'?',
						"%20",
						"%22",
						"%3c",		// <
						"%253c",	// <
						"%3e",		// >
						"%0e",		// >
						"%28",		// (
						"%29",		// )
						"%2528",	// (
						"%26",		// &
						"%24",		// $
						"%3f",		// ?
						"%3b",		// ;
						"%3d"		// =
					);

		if ( ! $relative_path)
		{
			$bad[] = './';
			$bad[] = '/';
		}

		$str = remove_invisible_characters($str, FALSE);
		return stripslashes(str_replace($bad, '', $str));
	}

	// ----------------------------------------------------------------

	/**
	 * 压缩被拆分的单词
	 *
	 * xss_clean()处理过程中的一个回调函数，用来删除空白，比如将下面的单词
	 * 中的空白删除：j a v a s c r i p t
	 *
	 * @param	type
	 * @return	type
	 */
	protected function _compact_exploded_words($matches)
	{
		return preg_replace('/\s+/s', '', $matches[1]).$matches[2];
	}

	// --------------------------------------------------------------------

	/*
	 * 删除HTML中的邪恶属性
	 *
	 * 删除邪恶属性：
	 *  - 一直删除直到遇到空格
	 *      例如，删除竖线之间的所有数据：
	 *      <a |style=document.write('hello');alert('world');| class=link>
	 *  - 删除引号包含的所有数据
	 *      例如，删除竖线之间的所有数据：
	 *      <a |style="document.write('hello'); alert('world');"| class="link">
	 *      
	 * @param string  $str 被检测的字符串
	 * @param boolean $is_image 是图片则为 TRUE
	 * @return string 删除邪恶属性之后的字符串
	 */
	protected function _remove_evil_attributes($str, $is_image)
	{
		// 所有javascript事件句柄(onload,onclick,onmouseover), style, xmlns
		$evil_attributes = array('on\w*', 'style', 'xmlns', 'formaction');

		if ($is_image === TRUE)
		{
			/*
			 * 由于 Adobe Photoshop 会将 XML 元数据以及命名空间添加到 JFIF 图片中，
			 * 所以我们在处理图片的时候不得不允许这方面的因素。
			 */
			unset($evil_attributes[array_search('xmlns', $evil_attributes)]);
		}
		
		do {
			$count = 0;
			$attribs = array();
			
			// 找到不带引号的非法属性字符串
			preg_match_all("/(".implode('|', $evil_attributes).")\s*=\s*([^\s]*)/is",  $str, $matches, PREG_SET_ORDER);
			
			foreach ($matches as $attr)
			{
				$attribs[] = preg_quote($attr[0], '/');
			}
			
			// 找到带有引号的非法字符串属性(042 和 047 是八进制的引号)
			preg_match_all("/(".implode('|', $evil_attributes).")\s*=\s*(\042|\047)([^\\2]*?)(\\2)/is",  $str, $matches, PREG_SET_ORDER);

			foreach ($matches as $attr)
			{
				$attribs[] = preg_quote($attr[0], '/');
			}

			// 将存在于html标签里面的非法属性字符串替换掉
			if (count($attribs) > 0)
			{
				$str = preg_replace("/<(\/?[^><]+?)([^A-Za-z\-])(".implode('|', $attribs).")([\s><])([><]*)/i", '<$1$2$4$5', $str, -1, $count);
			}
			
		} while ($count);
		
		return $str;
	}

	// --------------------------------------------------------------------

	/**
	 * 过滤不安全的HTML
	 *
	 * xss_clean()的回调函数，用以过滤不安全的HTML元素
	 * 
	 *
	 * @param	array
	 * @return	string
	 */
	protected function _sanitize_naughty_html($matches)
	{
		// 编码左尖括号
		$str = '&lt;'.$matches[1].$matches[2].$matches[3];

		// 继续编码尖括号以防止深层嵌套
		$str .= str_replace(array('>', '<'), array('&gt;', '&lt;'),
							$matches[4]);

		return $str;
	}

	// --------------------------------------------------------------------

	/**
	 * 删除JS链接
	 *
	 * xss_clean()的回调函数，用以过滤链接
	 * 该函数限制了 PCRE 回溯，使其更高性能的友好并且防止PREG_BACKTRACK_LIMIT_ERROR
	 * 在 PHP 5.2+ 的链接href字符串下被触发。
	 *
	 * @param	array
	 * @return	string
	 */
	protected function _js_link_removal($match)
	{
		$attributes = $this->_filter_attributes(str_replace(array('<', '>'), '', $match[1]));

		return str_replace($match[1], preg_replace("#href=.*?(alert\(|alert&\#40;|javascript\:|livescript\:|mocha\:|charset\=|window\.|document\.|\.cookie|<script|<xss|base64\s*,)#si", "", $attributes), $match[0]);
	}

	// --------------------------------------------------------------------

	/**
	 * 删除JS图像
	 *
	 * xss_clean()的回调函数，用以过滤图像标签
	 * 该函数限制了 PCRE 回溯，使其更高性能的友好并且防止PREG_BACKTRACK_LIMIT_ERROR
	 * 在 PHP 5.2+ 的图像标签src字符串下被触发。
	 *
	 * @param	array
	 * @return	string
	 */
	protected function _js_img_removal($match)
	{
		$attributes = $this->_filter_attributes(str_replace(array('<', '>'), '', $match[1]));

		return str_replace($match[1], preg_replace("#src=.*?(alert\(|alert&\#40;|javascript\:|livescript\:|mocha\:|charset\=|window\.|document\.|\.cookie|<script|<xss|base64\s*,)#si", "", $attributes), $match[0]);
	}

	// --------------------------------------------------------------------

	/**
	 * 属性转换
	 *
	 * 被用作过滤XSS的一个回调
	 *
	 * @param	array
	 * @return	string
	 */
	protected function _convert_attribute($match)
	{
		return str_replace(array('>', '<', '\\'), array('&gt;', '&lt;', '\\\\'), $match[0]);
	}

	// --------------------------------------------------------------------

	/**
	 * 属性过滤器
	 *
	 * 基于一致性和安全方面的考虑对标签属性进行过滤
	 *
	 * @param	string
	 * @return	string
	 */
	protected function _filter_attributes($str)
	{
		$out = '';

		if (preg_match_all('#\s*[a-z\-]+\s*=\s*(\042|\047)([^\\1]*?)\\1#is', $str, $matches))
		{
			foreach ($matches[0] as $match)
			{
				$out .= preg_replace("#/\*.*?\*/#s", '', $match);
			}
		}

		return $out;
	}

	// --------------------------------------------------------------------

	/**
	 * HTML实体解码回调
	 * 
	 *
	 * 被用作过滤XSS的一个回调
	 *
	 * @param	array
	 * @return	string
	 */
	protected function _decode_entity($match)
	{
		return $this->entity_decode($match[0], strtoupper(config_item('charset')));
	}

	// --------------------------------------------------------------------

	/**
	 * 验证URL实体
	 *
	 * 被xss_clean()调用
	 *
	 * @param 	string
	 * @return 	string
	 */
	protected function _validate_entities($str)
	{
		/*
		 * 保护URL中的GET变量
		 */

		 // 901119URL5918AMP18930PROTECT8198

		$str = preg_replace('|\&([a-z\_0-9\-]+)\=([a-z\_0-9\-]+)|i', $this->xss_hash()."\\1=\\2", $str);

		/*
		 * 验证标准字符实体
		 *
		 * 如果没有分号就添加一个。我们这样做是为了稍后将实体转换为ASCII。
		 *
		 */
		$str = preg_replace('#(&\#?[0-9a-z]{2,})([\x00-\x20])*;?#i', "\\1;\\2", $str);

		/*
		 * 验证 UTF16 两个字节的编码 （x00）
		 *
		 * 和上面一样，没有分号的加上分号。
		 *
		 */
		$str = preg_replace('#(&\#x?)([0-9A-F]+);?#i',"\\1\\2;",$str);

		/*
		 * 撤消URL中的GET变量的保护
		 */
		$str = str_replace($this->xss_hash(), '&', $str);

		return $str;
	}

	// ----------------------------------------------------------------------

	/**
	 * 绝不允许
	 *
	 * 针对xss_clean()的一个实用函数
	 *
	 * @param 	string
	 * @return 	string
	 */
	protected function _do_never_allowed($str)
	{
		foreach ($this->_never_allowed_str as $key => $val)
		{
			$str = str_replace($key, $val, $str);
		}

		foreach ($this->_never_allowed_regex as $key => $val)
		{
			$str = preg_replace("#".$key."#i", $val, $str);
		}

		return $str;
	}

	// --------------------------------------------------------------------

	/**
	 * 设置防止跨站请求伪造的保护COOKIE
	 *
	 * @return	string
	 */
	protected function _csrf_set_hash()
	{
		if ($this->_csrf_hash == '')
		{
			// 如果该cookie存在我们将使用其值
			// 我们没有必要加载每个页面都再次生成该cookie，因为有的页面
			// 中可能嵌套了自页面会导致其失败
			if (isset($_COOKIE[$this->_csrf_cookie_name]) &&
				$_COOKIE[$this->_csrf_cookie_name] != '')
			{
				return $this->_csrf_hash = $_COOKIE[$this->_csrf_cookie_name];
			}

			return $this->_csrf_hash = md5(uniqid(rand(), TRUE));
		}

		return $this->_csrf_hash;
	}

}
// END Security Class

/* End of file Security.php */
/* Location: ./system/libraries/Security.php */
